﻿using Aliyun.OSS;
using Aliyun.OSS.Common;
using Furion;
using Furion.DependencyInjection;
using Furion.FriendlyException;
using Furion.RemoteRequest;
using Furion.RemoteRequest.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using YFurion.Localization;
using YFurion.Localization.Extensions;

namespace YFurion.Utils
{
    public class DFSManager : IDFSManager, IScoped
    {
        private const string TYPE_OSS = "OSS";
        private const string TYPE_INFURA = "Infura";

        private readonly string[] _imageExts = { ".jpg", ".jpeg", ".png" };
        private readonly string[] _videoExts = { ".ogg", ".flv", ".avi", ".wmv", ".rmvb", ".mov", ".mp4" };

        private readonly ILocalizationManager _localizationManager;
        private readonly ILogger<DFSManager> _logger;

        /// <summary>
        /// 分布式存储类型
        /// </summary>
        private string _typeValue = TYPE_OSS;
        /// <summary>
        /// 图片存储空间
        /// </summary>
        private string _imageBucket = "ImageBucket";
        /// <summary>
        /// 视频存储空间
        /// </summary>
        private string _videoBucket = "VideoBucket";
        /// <summary>
        /// 文件存储空间
        /// </summary>
        private string _fileBucket = "FileBucket";

        public DFSManager
        (
            ILocalizationManager localizationManager,
            ILogger<DFSManager> logger
        )
        {
            _localizationManager = localizationManager;
            _logger = logger;
            InitConfig();
        }

        private void InitConfig()
        {
            IConfigurationSection dfsSecion = App.Configuration.GetSection("DFS");
            string typeValue = dfsSecion.GetValue<string>("Type");
            switch (typeValue)
            {
                case TYPE_OSS:
                    _typeValue = TYPE_OSS;
                    InitOssConfig(dfsSecion);
                    break;
                case TYPE_INFURA:
                    _typeValue = TYPE_INFURA;
                    InitInfuraConfig(dfsSecion);
                    break;
            }
        }

        public async Task<string> UploadImageAsync(IFormFile file, int maxSize, CancellationToken cancellationToken = default)
        {
            (string key, _) = await UploadAsync(file, maxSize, _imageBucket, (file, maxSize) =>
             {
                 string ext = Path.GetExtension(file.FileName);
                 if (!_imageExts.Contains(ext))
                 {
                     throw Oh(GetString("图片格式不对"));
                 }
                 if (file.Length > maxSize)
                 {
                     throw Oh(GetString("图片尺寸太大"));
                 }
             }, cancellationToken);
            return key;
        }

        public async Task<string> UploadVideoAsync(IFormFile file, int maxSize, CancellationToken cancellationToken = default)
        {
            (string key, _) = await UploadAsync(file, maxSize, _videoBucket, (file, maxSize) =>
            {
                string ext = Path.GetExtension(file.FileName);
                if (!_videoExts.Contains(ext))
                {
                    throw Oh(GetString("视频格式不对"));
                }
                if (file.Length > maxSize)
                {
                    throw Oh(GetString("视频尺寸太大"));
                }
            }, cancellationToken);
            return key;
        }

        public async Task<(string, object)> UploadAttachmentAsync(IFormFile file, string[] exts, int maxSize, CancellationToken cancellationToken = default)
        {
            (string key, object obj) = await UploadAsync(file, maxSize, _fileBucket, (file, maxSize) =>
            {
                string ext = Path.GetExtension(file.FileName);
                if (!exts.Contains(ext))
                {
                    throw Oh(GetString("文件格式不对"));
                }
                if (file.Length > maxSize)
                {
                    throw Oh(GetString("文件格式不对"));
                }
            }, cancellationToken);
            return (key, obj);
        }

        private async Task<(string, object)> UploadAsync(IFormFile file, int maxSize, string bucketName, Action<IFormFile, int> action, CancellationToken cancellationToken)
        {
            string key;
            object obj = null;
            switch (_typeValue)
            {
                case TYPE_OSS:
                default:
                    key = await UploadOSSAsync(file, maxSize, bucketName, action, cancellationToken);
                    break;
                case TYPE_INFURA:
                    (key, obj) = await UploadInfuraAsync(file, maxSize, action, cancellationToken);
                    break;
            }
            return (key, obj);
        }

        public async Task RemoveImageAsync(string key, CancellationToken cancellationToken = default)
        {
            await RemoveAsync(_imageBucket, key, cancellationToken);
        }

        public async Task RemoveVideoAsync(string key, CancellationToken cancellationToken = default)
        {
            await RemoveAsync(_videoBucket, key, cancellationToken);
        }

        public async Task RemoveAttachmentAsync(string key, CancellationToken cancellationToken = default)
        {
            await RemoveAsync(_fileBucket, key, cancellationToken);
        }

        private async Task RemoveAsync(string bucketName, string key, CancellationToken cancellationToken)
        {
            switch (_typeValue)
            {
                case TYPE_OSS:
                default:
                    await RemoveOSSAsync(bucketName, key, cancellationToken);
                    break;
                case TYPE_INFURA:
                    //暂时没有找到删除接口
                    break;
            }
        }

        public async Task<byte[]> DownloadFileAsync(string key, long offset, long length, CancellationToken cancellationToken = default)
        {
            byte[] buffer;
            switch (_typeValue)
            {
                case TYPE_OSS:
                default:
                    buffer = await DownloadOSSFileAsync(key, offset, length, cancellationToken);
                    break;
                case TYPE_INFURA:
                    buffer = await DownloadInfuraFileAsync(key, cancellationToken);
                    break;
            }
            return buffer;
        }

        #region 阿里云OSS
        /// <summary>
        /// 域名
        /// </summary>
        private string _endpoint;

        /// <summary>
        /// 访问Id
        /// </summary>
        private string _accessKeyId;

        /// <summary>
        /// 访问密钥
        /// </summary>
        private string _accessKeySecret;

        /// <summary>
        /// 保存断点续传的中间状态，用于失败后继续上传
        /// </summary>
        private string _checkpointDir;

        /// <summary>
        /// 初始化对象存储服务配置
        /// </summary>
        private void InitOssConfig(IConfigurationSection dfsSecion)
        {
            IConfigurationSection configurationSection = dfsSecion.GetSection(TYPE_OSS);
            _endpoint = configurationSection.GetValue<string>("Endpoint");
            _accessKeyId = configurationSection.GetValue<string>("AccessKeyId");
            _accessKeySecret = configurationSection.GetValue<string>("AccessKeySecret");
            _imageBucket = configurationSection.GetValue<string>("ImageBucket");
            _videoBucket = configurationSection.GetValue<string>("VideoBucket");
            _fileBucket = configurationSection.GetValue<string>("FileBucket");
            _checkpointDir = configurationSection.GetValue<string>("CheckpointDir");
            if (!Directory.Exists(_checkpointDir))
            {
                Directory.CreateDirectory(_checkpointDir);
            }
        }

        /// <summary>
        /// 异步上传对象存储服务文件
        /// </summary>
        /// <param name="file">表单文件对象</param>
        /// <param name="maxSize">最大文件大小</param>
        /// <param name="bucketName">存储空间名</param>
        /// <param name="action"></param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>上传后的文件名</returns>
        private async Task<string> UploadOSSAsync(IFormFile file, int maxSize, string bucketName, Action<IFormFile, int> action, CancellationToken cancellationToken)
        {
            action(file, maxSize);
            string errorMessage = GetString("上传失败");
            string key;
            PutObjectResult result;
            try
            {
                key = GetFilePath(file.FileName);
                OssClient ossClient = GetOssClient();
                UploadObjectRequest request = new UploadObjectRequest(bucketName, key, file.OpenReadStream());
                // 指定上传的分片大小。
                request.PartSize = 8 * 1024 * 1024;
                // 指定并发线程数。
                request.ParallelThreadCount = 3;
                // checkpointDir保存断点续传的中间状态，用于失败后继续上传。如果checkpointDir为null，断点续传功能不会生效，每次失败后都会重新上传。
                request.CheckpointDir = _checkpointDir;
                // 进度条事件。
                request.StreamTransferProgress += UploadProgressCallback;
                result = ossClient.ResumableUploadObject(request);
            }
            catch (OssException ex)
            {
                string message = string.Format("Upload failed with error code: {0}; Error info: {1}. \nRequestID:{2}\tHostID:{3}", ex.ErrorCode, ex.Message, ex.RequestId, ex.HostId);
                _logger.LogError(message, ex.StackTrace);
                throw Oh(errorMessage);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Upload failed with error info: {ex.Message}", ex.StackTrace);
                throw Oh(errorMessage);
            }
            if (result.HttpStatusCode != HttpStatusCode.OK)
            {
                _logger.LogError($"Upload failed with error http status code: {result.HttpStatusCode}");
                throw Oh(errorMessage);
            }
            return await Task.FromResult(key);
        }

        /// <summary>
        /// 获取上传进度
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private static void UploadProgressCallback(object sender, StreamTransferProgressArgs args)
        {
            Console.WriteLine("ProgressCallback - Progress: {0}%, TotalBytes:{1}, TransferredBytes:{2} ",
                args.TransferredBytes * 100 / args.TotalBytes, args.TotalBytes, args.TransferredBytes);
        }

        /// <summary>
        /// 异步删除对象存储服务文件
        /// </summary>
        /// <param name="bucketName">存储空间名</param>
        /// <param name="key">文件名</param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns></returns>
        private Task RemoveOSSAsync(string bucketName, string key, CancellationToken cancellationToken)
        {
            try
            {
                OssClient ossClient = GetOssClient();
                ossClient.DeleteObject(bucketName, key);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Delete object failed. {ex.Message}", ex.StackTrace);
            }
            return Task.CompletedTask;
        }

        private async Task<byte[]> DownloadOSSFileAsync(string key, long start, long count, CancellationToken cancellationToken)
        {
            try
            {
                OssClient ossClient = GetOssClient();
                GetObjectRequest getObjectRequest = new GetObjectRequest(_fileBucket, key);
                // 对于大小为1000 Bytes的文件，正常的字节范围为0~999。
                // 获取0~999字节范围内的数据，包括0和999，共1000个字节的数据。如果指定的范围无效（比如开始或结束位置的指定值为负数，或指定值大于文件大小），则下载整个文件。
                getObjectRequest.SetRange(start, start + count);
                OssObject obj = ossClient.GetObject(getObjectRequest);
                // 下载数据并写入文件。
                using (Stream requestStream = obj.Content)
                {
                    byte[] buffer = new byte[count];
                    await requestStream.ReadAsync(buffer, 0, (int)count, cancellationToken);
                    return buffer;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Get object failed. {0}", ex.Message);
                throw Oh(GetString("下载失败"));
            }
        }

        /// <summary>
        /// 获取对象存储服务客户端
        /// </summary>
        /// <returns></returns>
        private OssClient GetOssClient()
        {
            OssClient ossClient = new OssClient(_endpoint, _accessKeyId, _accessKeySecret);
            return ossClient;
        }

        /// <summary>
        /// 获取上传的文件路径
        /// 目录: 根据年月日归档
        /// 文件名: 时间戳 + 随机数
        /// </summary>
        /// <param name="fileName">上传文件名</param>
        /// <returns></returns>
        private string GetFilePath(string fileName)
        {
            string num = new Random().Next(1, 1000).ToString().PadLeft(4, '0');
            DateTimeOffset dateTime = DateTimeOffset.Now;
            return $"{dateTime.ToString("yyyy")}/{dateTime.ToString("MM")}/{dateTime.ToString("dd")}/{dateTime.ToUnixTimeMilliseconds()}{num}{Path.GetExtension(fileName)}";
        }
        #endregion

        #region Infura
        private string _infuraUrl;

        private string _tokenBase64;

        private void InitInfuraConfig(IConfigurationSection dfsSecion)
        {
            IConfigurationSection configurationSection = dfsSecion.GetSection(TYPE_INFURA);
            _infuraUrl = configurationSection.GetValue<string>("Url");
            string projectId = configurationSection.GetValue<string>("ProjectId");
            string apiKeySecret = configurationSection.GetValue<string>("ApiKeySecret");
            byte[] tokenBytes = Encoding.UTF8.GetBytes($"{projectId}:{apiKeySecret}");
            _tokenBase64 = Convert.ToBase64String(tokenBytes);
        }

        /// <summary>
        /// 异步上传Infura文件
        /// </summary>
        /// <param name="file">表单文件对象</param>
        /// <param name="maxSize">最大文件大小</param>
        /// <param name="action"></param>
        /// <param name="cancellationToken">取消令牌</param>
        /// <returns>上传后的文件名</returns>
        private async Task<(string, object)> UploadInfuraAsync(IFormFile file, int maxSize, Action<IFormFile, int> action, CancellationToken cancellationToken)
        {
            action(file, maxSize);
            InfuraUploadResult result;
            Stream stream = null;
            try
            {
                stream = file.OpenReadStream();
                byte[] bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
                result = await $"{_infuraUrl}/add".SetContentType("multipart/form-data").SetHeaders(GetAuthorizationHeader()).SetFiles(HttpFile.Create("file", bytes, file.FileName)).PostAsAsync<InfuraUploadResult>(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Upload IPFS failed with error info: {ex.Message}", ex.StackTrace);
                throw Oh(GetString("上传失败"));
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
            return (result.Hash, result);
        }

        private async Task<byte[]> DownloadInfuraFileAsync(string key, CancellationToken cancellationToken)
        {
            byte[] buffer;
            try
            {
                buffer = await $"{_infuraUrl}/cat?arg={key}".SetHeaders(GetAuthorizationHeader()).PostAsByteArrayAsync(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError("Get IPFS object failed. {0}", ex.Message);
                throw Oh(GetString("下载失败"));
            }
            return buffer;
        }

        private object GetAuthorizationHeader()
        {
            return new { Authorization = $"Basic {_tokenBase64}" };
        }
        #endregion

        private Exception Oh(string errorMessage)
        {
            return Oops.Oh(errorMessage);
        }

        private string GetString(string name)
        {
            return _localizationManager.GetCurrentString(name);
        }
    }
}